package com.cltech.frame.query;

import com.cltech.frame.utils.AopTargetUtils;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.query.AbstractJpaQuery;
import org.springframework.data.jpa.repository.query.JpaParameters;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.CollectionUtils;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * SmartJpaQuery.
 *
 * @author guanbo
 * @version 1.0.0.0
 */
public class SmartJpaQuery extends AbstractJpaQuery {
    /**
     * Logger available to subclasses.
     */
    private final Log logger = LogFactory.getLog(getClass());
    /**
     * useJpaSpec.
     */
    private boolean useJpaSpec = true;
    /**
     * jpaQueryMethod.
     */
    private JpaQueryMethod jpaQueryMethod;
    /**
     * method.
     */
    private Method method;
    /**
     * em
     */
    private EntityManager em;

    /**
     * Creates a new {@link AbstractJpaQuery} from the given {@link JpaQueryMethod}.
     *
     * @param method         class method
     * @param jpaQueryMethod jpa query method
     * @param em             com.bidding.entity manager
     */
    SmartJpaQuery(final Method method, final JpaQueryMethod jpaQueryMethod, final EntityManager em) {
        super(jpaQueryMethod, em);
        this.jpaQueryMethod = jpaQueryMethod;
        this.method = method;
        this.em = em;
    }

    @Override
    protected Query doCreateQuery(final Object[] values) {
        final String queryString = getQueryString(values);
        final JpaParameters parameters = getQueryMethod().getParameters();
        final ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
        final String sortedQueryString = QueryUtils
                .applySorting(queryString, accessor.getSort(), QueryUtils.detectAlias(queryString));
        final Query query = bind(createJpaQuery(sortedQueryString), values);
        if (parameters.hasPageableParameter()) {
            final Pageable pageable = (Pageable) (values[parameters.getPageableIndex()]);
            if (pageable != null) {
                query.setFirstResult((int) pageable.getOffset());
                query.setMaxResults(pageable.getPageSize());
            }
        }
        return query;
    }

    /**
     * get sql.
     *
     * @param values method params
     * @return sql
     */
    private String getQueryString(final Object[] values) {
        return getQueryFromAnnotation(values);
    }

    /**
     * get sql,if annotation value is not freemarkerTemplate {@link SmartJpaQuery#isFreemarkerTemplate}, will direct
     * return it,
     * but process as StringTemplate {@link SmartJpaQuery#processFreemarkerTemplate} then return processed result.
     *
     * @param values method params
     * @return sql
     */
    private String getQueryFromAnnotation(final Object[] values) {
        final String sql = (String) getMergedOrDefaultAnnotationValue("value", SmartQuery.class, String.class);
        if (isFreemarkerTemplate()) {
            return processFreemarkerTemplate(sql, getParams(values));
        } else {
            return sql;
        }
    }


    private Boolean isFreemarkerTemplate() {
        return (Boolean) getMergedOrDefaultAnnotationValue("freemarkerTemplate", SmartQuery.class, Boolean.class);
    }

    private Boolean isNativeQuery() {
        return (Boolean) getMergedOrDefaultAnnotationValue("nativeQuery", SmartQuery.class, Boolean.class);
    }

    /**
     * process templateContent.
     *
     * @param templateContent templateContent
     * @param model           method params
     * @return sql
     */
    private String processFreemarkerTemplate(final String templateContent, final Map<String, Object> model) {
        final String templateName = "sqlTemplate_" + getEntityName() + "_" + getMethodName();
        final Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        final StringTemplateLoader stringLoader = new StringTemplateLoader();
        stringLoader.putTemplate(templateName, templateContent);
        cfg.setTemplateLoader(stringLoader);
        StringWriter writer = null;
        try {
            final Template template = cfg.getTemplate(templateName, "utf-8");
            writer = new StringWriter();
            template.process(model, writer);
        } catch (final IOException | TemplateException e) {
            logger.info("SQL解析错误", e);
        }
        return writer.toString();
    }

    private String getEntityName() {
        return getQueryMethod().getEntityInformation().getJavaType().getSimpleName();
    }

    private String getMethodName() {
        return getQueryMethod().getName();
    }

    /**
     * get attribute value from annotation.
     *
     * @param attribute      attribute
     * @param annotationType annotationType
     * @param targetType     targetType
     * @return sql
     */
    private Object getMergedOrDefaultAnnotationValue(final String attribute, final Class annotationType,
                                                     final Class targetType) {
        final Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
        return annotation == null
                ? targetType.cast(AnnotationUtils.getDefaultValue(annotationType, attribute))
                : targetType.cast(AnnotationUtils.getValue(annotation, attribute));
    }

    /**
     * getParams.
     *
     * @param values values
     * @return ParamsMap
     */
    private Map<String, Object> getParams(final Object[] values) {
        final JpaParameters parameters = getQueryMethod().getParameters();
        //gen model
        Map<String, Object> params = new HashMap<>();
        for (int i = 0; i < parameters.getNumberOfParameters(); i++) {
            final Object value = values[i];
            final Parameter parameter = parameters.getParameter(i);
            if (value != null && canBindParameter(parameter)) {
                if (!QueryBuilder.isValidValue(value)) {
                    continue;
                }
                final Class<?> clz = value.getClass();
                if (clz.isPrimitive() || String.class.isAssignableFrom(clz) || Number.class.isAssignableFrom(clz)
                        || clz.isArray() || Collection.class.isAssignableFrom(clz) || clz.isEnum() || Date.class
                        .isAssignableFrom(clz)) {
                    if (!parameter.getName().isPresent()) {
                        params.put((parameter.getIndex() + 1) + "", value);
                    } else {
                        params.put(parameter.getName().get(), value);
                    }
                } else {
                    params = QueryBuilder.toParams(value);
                }
            }
        }
        return params;
    }

    /**
     * createJpaQuery.
     *
     * @param queryString queryString
     * @return Query
     */
    private Query createJpaQuery(final String queryString) {
        final Class<?> objectType = getQueryMethod().getReturnedObjectType();
        //get original proxy query.
        Query oriProxyQuery;
        //must be hibernate QueryImpl
        org.hibernate.query.Query query = null;
        if (isNativeQuery()) {
            oriProxyQuery = getEntityManager().createNativeQuery(queryString);
        } else {
            //hasConstructor
            if (!hasConstructorExpression(queryString)) {
                oriProxyQuery = getEntityManager().createQuery(queryString);
            } else {
                return getEntityManager().createQuery(queryString);
            }
        }

        try {
            query = (org.hibernate.query.Query) AopTargetUtils.getTarget(oriProxyQuery);
        } catch (final Exception e) {
            e.printStackTrace();
        }
        //find generic type
        final ClassTypeInformation<?> ctif = ClassTypeInformation.from(objectType);
        TypeInformation<?> actualType = ctif.getActualType();
        if (actualType == null) {
            actualType = ctif.getRawTypeInformation();
        }
        final Class<?> genericType = actualType.getType();
        if (genericType != null && genericType != Void.class) {
            QueryBuilder.transform(query, genericType);
        }
        //return the original proxy query, for a series of JPA actions, e.g.:close em.
        return oriProxyQuery;
    }

    public boolean isDefaultProjection(final String queryString) {
        return QueryUtils.getProjection(queryString).equalsIgnoreCase(QueryUtils.detectAlias(queryString));
    }

    public boolean hasConstructorExpression(final String queryString) {
        return QueryUtils.hasConstructorExpression(queryString);
    }

    @Override
    @SuppressWarnings("unchecked")
    protected TypedQuery<Long> doCreateCountQuery(final Object[] values) {
        if (isNativeQuery()) {
            final TypedQuery query = (TypedQuery) getEntityManager()
                    .createNativeQuery(QueryBuilder.toCountQuery(getQueryString(values)));
            bind(query, values);
            return query;
        } else {
            final TypedQuery query = (TypedQuery) getEntityManager()
                    .createQuery(QueryUtils.createCountQueryFor(getQueryString(values)));
            bind(query, values);
            return query;
        }
    }

    /**
     * bind params.
     *
     * @param query  query
     * @param values values
     * @return Query
     */
    private Query bind(final Query query, final Object[] values) {
        //get proxy target if exist.
        //must be hibernate QueryImpl
        org.hibernate.query.Query targetQuery = null;
        try {
            targetQuery = (org.hibernate.query.Query) AopTargetUtils.getTarget(query);
        } catch (final Exception e) {
            e.printStackTrace();
        }
        final org.hibernate.query.Query sqlQuery = targetQuery;

        final Map<String, Object> params = getParams(values);
        if (!CollectionUtils.isEmpty(params)) {
            QueryBuilder.setParams(sqlQuery, params);
        }
        return query;
    }

    /**
     * canBindParameter.
     *
     * @param parameter parameter
     * @return boolean
     */
    private boolean canBindParameter(final Parameter parameter) {
        return parameter.isBindable();
    }
}
